package com.project.common.data.mybatis.metadata;

import com.project.common.data.mybatis.annotation.Column;
import com.project.common.data.mybatis.annotation.Table;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.JdbcType;

import java.beans.Transient;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TableMetadata {
    private static final int DEFAULT_CACHE_LIMIT = 256;
    private static final Pattern PATTERN_CAMEL_IDENTIFY = Pattern.compile("[_|\\-]([a-z])");
    private static final Pattern PATTERN_DASHER_IDENTIFY = Pattern.compile("[A-Z]");
    private static final String TABLE_NAME_PREFIX = "t_";
    private static final String DEFAULT_KEY_PROPERTY_NAME = "id";
    private static final Map<Class<?>, TableMetadata> TABLE_CACHE =
            new LinkedHashMap<Class<?>, TableMetadata>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<Class<?>, TableMetadata> eldest) {
                    return size() > DEFAULT_CACHE_LIMIT;
                }
            };
    /**
     * 对应的实体类
     */
    private Class<?> entityClass;
    /**
     * 数据库表名
     */
    private String tableName;
    /**
     * 列信息
     */
    private TableColumn[] columns;
    /**
     * 主键列名
     */
    private TableColumn keyColumn;

    private TableMetadata(Class<?> entityClass) {
        this.entityClass = entityClass;
        initialize();
    }

    public static TableMetadata forClass(Class<?> entityClass) {
        synchronized (TABLE_CACHE) {
            return TABLE_CACHE.computeIfAbsent(entityClass, TableMetadata::new);
        }
    }

    private static String camelize(String original) {
        if (StringUtils.isBlank(original)) {
            return original;
        }
        Matcher matcher = PATTERN_CAMEL_IDENTIFY.matcher(original.toLowerCase());
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(buffer, matcher.group(1).toUpperCase());
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    private static String dasherize(String original) {
        return dasherize(original, "_");
    }

    private static String dasherize(String original, String split) {
        if (StringUtils.isBlank(original)) {
            return original;
        }
        Matcher matcher = PATTERN_DASHER_IDENTIFY.matcher(original);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(buffer, split + matcher.group(0).toLowerCase());
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    private void initialize() {
        if (entityClass == null) {
            return;
        }
        // 设置表名
        Table table = entityClass.getAnnotation(Table.class);
        if (table != null) {
            this.tableName = table.value();
        }
        if (StringUtils.isBlank(this.tableName)) {
            this.tableName = TABLE_NAME_PREFIX + dasherize(entityClass.getSimpleName());
        }
        Field[] fields = entityClass.getDeclaredFields();
        List<TableColumn> tableColumns = new ArrayList<>();
        for (Field field : fields) {
            if (Modifier.isStatic(field.getModifiers())
                    || field.isAnnotationPresent(Transient.class)) {
                continue;
            }
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                if (!column.exist()) {
                    continue;
                }
                TableColumn tableColumn = new TableColumn();
                if (StringUtils.isBlank(column.value())) {
                    tableColumn.setName(dasherize(field.getName()));
                } else {
                    tableColumn.setName(column.value());
                }
                tableColumn.setProperty(field.getName());
                tableColumn.setType(field.getType());
                tableColumn.setKey(column.key());
                tableColumn.setUpdateToNull(column.updateToNull());
                String typeCode = column.type();
                tableColumn.setJdbcType(this.detectJdbcType(typeCode, field.getType()));
                if (tableColumn.isKey()) {
                    this.keyColumn = tableColumn;
                }
                tableColumns.add(tableColumn);
            } else {
                TableColumn tableColumn = new TableColumn();
                tableColumn.setName(dasherize(field.getName()));
                tableColumn.setProperty(field.getName());
                tableColumn.setType(field.getType());
                tableColumn.setJdbcType(this.detectJdbcType("", field.getType()));
                tableColumn.setKey(field.getName().equals(DEFAULT_KEY_PROPERTY_NAME));
                if (tableColumn.isKey()) {
                    this.keyColumn = tableColumn;
                }
                tableColumns.add(tableColumn);
            }
        }
        this.columns = tableColumns.toArray(new TableColumn[0]);
    }

    private JdbcType detectJdbcType(String typeCode, Class<?> javaType) {
        if (StringUtils.isNotBlank(typeCode)) {
            return JdbcType.valueOf(typeCode);
        }
        JdbcType jdbcType = null;
        String typeName = javaType.getSimpleName();
        switch (typeName) {
            case "boolean":
            case "Boolean":
                jdbcType = JdbcType.BIT;
                break;
            case "byte":
            case "Byte":
                jdbcType = JdbcType.TINYINT;
                break;
            case "short":
            case "Short":
                jdbcType = JdbcType.SMALLINT;
                break;
            case "int":
            case "Integer":
                jdbcType = JdbcType.INTEGER;
                break;
            case "long":
            case "Long":
                // jdbcType = JdbcType.BIGINT;
                jdbcType = JdbcType.NUMERIC;
                break;
            case "double":
            case "Double":
                jdbcType = JdbcType.DOUBLE;
                break;
            case "byte[]":
            case "Byte[]":
                jdbcType = JdbcType.BLOB;
                break;
            case "String":
                jdbcType = JdbcType.VARCHAR;
                break;
            case "BigDecimal":
                jdbcType = JdbcType.DECIMAL;
                break;
            case "Date":
                jdbcType = JdbcType.DATE;
                break;
            case "Time":
                jdbcType = JdbcType.TIME;
                break;
            case "Timestamp":
                jdbcType = JdbcType.TIMESTAMP;
                break;
        }
        return jdbcType;
    }

    public Class<?> getEntityClass() {
        return entityClass;
    }

    public String getTableName() {
        return tableName;
    }

    public TableColumn[] getColumns() {
        return columns;
    }

    public TableColumn getKeyColumn() {
        return keyColumn;
    }
}
